chore: add OHLCVService for real-time candlestick WebSocket streaming#8695
Conversation
2d69c0f to
bd0d4d2
Compare
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
| */ | ||
| async subscribe(options: OHLCVSubscriptionOptions): Promise<void> { | ||
| const channel = this.#buildChannel(options); | ||
| return this.#withChannelLock(channel, () => this.#subscribeInner(channel)); |
There was a problem hiding this comment.
I think you can use import { Mutex } from 'async-mutex'; its already used in other Controllers
|
@metamaskbot publish-preview |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 337abd9. Configure here.
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
Kriys94
left a comment
There was a problem hiding this comment.
LGTM. Thanks a lot @sahar-fehri
## **Description** Related to: https://www.notion.so/metamask-consensys/OHLCV-WebSocket-Integration-UI-Implementation-Guide-346f86d67d6880b6a70fc3be0f0c34b9 Wires `OHLCVService` from `@metamask/core-backend` into the Engine and creates a `useOHLCVRealtime` hook that streams live candlestick updates to the advanced chart via the existing `realtimeBar` prop on `AdvancedChart`. **Why:** The advanced chart currently only renders historical data fetched via the REST OHLCV API. Users see stale candles until they navigate away and back. Real-time streaming via WebSocket keeps the chart live with 5-second heartbeat updates. **How:** Follows the exact same Engine wiring pattern as `AccountActivityService` — messenger, init function, Engine registration. The new `useOHLCVRealtime` hook subscribes to `OHLCVService:barUpdated` events, filters by channel, and converts the WS bar format (timestamp in Unix seconds) to the chart's expected format (time in milliseconds). ## Manual Test Plan ### Prerequisites - MetaMask Mobile connected to a wallet with tokens - `backendWebSocketConnection` feature flag enabled ### Adding console.log statements to the mobile hook **1. Inside `handleBarUpdated`** — after the channel guard: ```ts const handleBarUpdated = (payload: { channel: string; bar: WSOHLCVBar }) => { if (payload.channel === channelRef.current) { console.log( // ← ADD `[OHLCV-WS] Bar received — channel=${payload.channel}, close=${payload.bar.close}, ts=${payload.bar.timestamp}`, ); lastMessageTimeRef.current = Date.now(); ... ``` **2. Inside `handleSubscriptionError`** — first line of the callback: ```ts const handleSubscriptionError = (payload: { channel: string; error: string; operation: string }) => { console.log( // ← ADD `[OHLCV-WS] Subscription error on ${payload.channel}: ${payload.error} (${payload.operation})`, ); }; ``` **3. Inside `handleChainStatusChanged`** — after the `chainIds.includes` guard: ```ts if (payload.chainIds.includes(chainId)) { console.log( // ← ADD `[OHLCV-WS] Chain status changed — chainId=${chainId}, status=${payload.status}`, ); chainDownRef.current = payload.status === 'down'; } ``` **4. Inside `pollLatest`** — first line of the function: ```ts const pollLatest = async () => { pollingAbortRef.current?.abort(); const controller = new AbortController(); pollingAbortRef.current = controller; console.log('[OHLCV-WS] Polling /latest via REST fallback'); // ← ADD ... ``` **5. Inside the staleness `setInterval`** — when `isStale || chainDown`: ```ts if (isStale || chainDownRef.current) { console.log( // ← ADD `[OHLCV-WS] Stream stale or chain down — isStale=${isStale}, chainDown=${chainDownRef.current}, elapsed=${elapsed}ms`, ); pollLatest(); } ``` **6. Inside the debounce `setTimeout`** — first line: ```ts debounceTimerRef.current = setTimeout(async () => { console.log( // ← ADD `[OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for ${channel}`, ); try { await Engine.controllerMessenger.call('OHLCVService:subscribe', { ... }); ... ``` **7. In the cleanup `return` function** — first line: ```ts return () => { console.log( // ← ADD `[OHLCV-WS] Cleanup — channel=${channel}, wasSubscribed=${subscribedRef.current}`, ); cancelledRef.current = true; ... ``` ### Enabling core logs in the debugger By default, core `OHLCVService` logs use `projectLogger` (the `debug` package) and won't appear in the React Native debugger. To make them visible, open: ``` node_modules/@metamask/core-backend/dist/ws/ohlcv/OHLCVService.cjs ``` Find this line (near the top, around line 30): ```js const log = (0, logger_1.createModuleLogger)(logger_1.projectLogger, SERVICE_NAME); ``` Replace with: ```js const log = (...args) => console.log('[OHLCV-WS]', ...args); ``` Now all core logs will appear in the debugger with the `[OHLCV-WS]` prefix, alongside the mobile hook logs. Revert with `yarn install` when done. --- ## Group A — No Code Changes (Just Tap and Observe) --- ### Scenario 1: Basic WebSocket Subscription **Steps:** 1. Open Token Details for a supported token (e.g., ETH on Base) 2. Wait for historical chart to load 3. Observe logs **Expected logs:** ``` [OHLCV-WS] OHLCV-WS: Initializing — registering system-notifications callback [OHLCV-WS] OHLCV-WS: Resubscribing active channels after reconnect {count: 0} [OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.15m.eur [OHLCV-WS] OHLCV-WS: Subscribe succeeded — new WS subscription created {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'} [OHLCV-WS] Bar received — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, close=1986.69, ts=1778538600 ``` > **Note:** `Resubscribing active channels after reconnect {count: 0}` appears at app boot because `AccountActivityService` opened the shared WebSocket first. OHLCVService hears the `CONNECTED` event and checks for channels to restore — finds zero since no subscription exists yet. This is normal. **Verify:** Bars continue arriving every ~5s with updating `close` prices. --- ### Scenario 2: Navigate Away (Unsubscribe + Grace Period) **Steps:** 1. From Scenario 1, press back to leave Token Details 2. Wait 3+ seconds 3. Observe logs **Expected logs:** ``` [OHLCV-WS] Cleanup — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, wasSubscribed=true [OHLCV-WS] OHLCV-WS: Grace period expired — performing actual WS unsubscribe {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'} [OHLCV-WS] OHLCV-WS: WS unsubscribe completed {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'} ``` **Verify:** No more bar updates after grace period expires. --- ### Scenario 3: Rapid Navigation (Grace Period Cancel) **Steps:** 1. Open Token Details for Token A, wait for subscription 2. Navigate back 3. Immediately re-open Token A (within 3 seconds) **Expected logs:** ``` [OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.15m.eur [OHLCV-WS] OHLCV-WS: Subscribe succeeded — new WS subscription created {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'} [OHLCV-WS] Cleanup — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, wasSubscribed=true [OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.15m.eur [OHLCV-WS] OHLCV-WS: Cancelled grace-period unsubscribe, bumped refCount {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur', refCount: 1} ``` **Verify:** `Cancelled grace-period unsubscribe, bumped refCount` appears — subscription was reused without a server roundtrip. --- ### Scenario 4: Switch Between Tokens **Steps:** 1. Open Token Details for Token A (e.g. ETH on Base), wait for subscription 2. Navigate back to token list 3. Open Token B (e.g. MNT on Ethereum) **Expected logs:** ``` [OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.15m.eur [OHLCV-WS] OHLCV-WS: Subscribe succeeded — new WS subscription created {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'} [OHLCV-WS] Bar received — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, close=1988.32, ts=1778539500 [OHLCV-WS] Cleanup — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, wasSubscribed=true [OHLCV-WS] OHLCV-WS: Grace period expired — performing actual WS unsubscribe {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'} [OHLCV-WS] OHLCV-WS: WS unsubscribe completed {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'} [OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:1/erc20:0x3c3a...15m.eur [OHLCV-WS] OHLCV-WS: Subscribe succeeded — new WS subscription created {channel: 'market-data.v1.eip155:1/erc20:0x3c3a...15m.eur'} [OHLCV-WS] Bar received — channel=market-data.v1.eip155:1/erc20:0x3c3a...15m.eur, close=0.59, ts=1778539500 ``` **Verify:** Token A fully unsubscribes (grace period expires). Token B gets its own subscription and bars flow. --- ### Scenario 5: Rapid Time Range Switching **Steps:** 1. Open Token Details, wait for bars on default time range (15m) 2. Rapidly switch between time ranges (e.g. 1H → 1D → 1W → 1H) **Expected logs (showing one switch cycle: 15m → 1h):** ``` [OHLCV-WS] Cleanup — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, wasSubscribed=true [OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.1h.eur [OHLCV-WS] OHLCV-WS: Flushing grace-period channel before new subscribe {flushedChannel: '...15m.eur', newChannel: '...1h.eur'} [OHLCV-WS] OHLCV-WS: Grace period expired — performing actual WS unsubscribe {channel: '...15m.eur'} [OHLCV-WS] OHLCV-WS: WS unsubscribe completed {channel: '...15m.eur'} [OHLCV-WS] OHLCV-WS: Subscribe succeeded — new WS subscription created {channel: '...1h.eur'} ``` This pattern repeats for each switch (1h → 1d → 1h → 15m → 1m). Each time, the old channel is flushed immediately before the new subscribe — no accumulation, no server rejections. **Verify:** Every subscribe succeeds (`Subscribe succeeded`). `Flushing grace-period channel` appears before each new subscribe. Bars flow on the final time range. --- ### Scenario 6: App Background / Foreground **Steps:** 1. Open Token Details for a supported token, wait for bars to flow 2. Press home button (send app to background) 3. Wait ~10 seconds 4. Bring app back to foreground **Expected logs:** ``` [OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.15m.eur [OHLCV-WS] OHLCV-WS: Subscribe succeeded — new WS subscription created {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'} [OHLCV-WS] Bar received — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, close=1987.42, ts=1778540400 [OHLCV-WS] Bar received — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, close=1987.37, ts=1778540400 — app sent to background, then brought back — [OHLCV-WS] OHLCV-WS: Resubscribing active channels after reconnect {count: 1} [OHLCV-WS] OHLCV-WS: Resubscription succeeded {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'} [OHLCV-WS] Bar received — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, close=1987.18, ts=1778540400 ``` **Verify:** `Resubscribing active channels after reconnect {count: 1}` appears after foregrounding. Bars resume automatically without user interaction. --- ### Scenario 7: Unsupported Token (No OHLCV Data) **Steps:** 1. Open Token Details for a token with no OHLCV API data **Expected:** No WS subscription, falls back to legacy line chart. --- ## Group B — Requires Changing DEV Constants in `useOHLCVRealtime.ts` > After testing, set both constants back to `0` before committing. --- ### Scenario 8: WebSocket Disconnect → REST Polling Fallback **What this tests:** The WebSocket connection drops and stays disconnected. After the staleness threshold (30s) is exceeded, the hook falls back to polling REST. #### Code to add In `useOHLCVRealtime.ts`, set the DEV constant: ```ts const DEV_SIMULATE_WS_DISCONNECT_AFTER_MS = 10000; // ← ACTIVE ``` The simulation code in the hook must call `disconnect` (clean shutdown, **not** `forceReconnection`): ```ts if (DEV_SIMULATE_WS_DISCONNECT_AFTER_MS > 0) { setTimeout(() => { console.log( `[OHLCV-WS] DEV: Simulating WS disconnect (no reconnect) after ${DEV_SIMULATE_WS_DISCONNECT_AFTER_MS}ms`, ); Engine.controllerMessenger.call( 'BackendWebSocketService:disconnect' as never, ); }, DEV_SIMULATE_WS_DISCONNECT_AFTER_MS); } ``` #### How it works After 10s, calls `BackendWebSocketService:disconnect` (clean shutdown, no auto-reconnect). The WS stays dead. After 30s with no bars, staleness triggers REST polling every 15s. #### Steps to test 1. Set the constants as shown above 2. Rebuild / hot-reload the app 3. Open Token Details for a supported token 4. Wait for bars to start flowing (~5s) 5. At 10s, the simulated disconnect fires automatically 6. Wait ~30s for the staleness threshold 7. Observe REST fallback polling in logs #### Expected logs: ``` [OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.15m.eur [OHLCV-WS] OHLCV-WS: Subscribe succeeded — new WS subscription created {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur'} [OHLCV-WS] Bar received — channel=market-data.v1.eip155:8453/slip44:60.15m.eur, close=1985.08, ts=1778540400 [OHLCV-WS] DEV: Simulating WS disconnect (no reconnect) after 10000ms [OHLCV-WS] Stream stale or chain down — isStale=true, chainDown=false, elapsed=38975ms [OHLCV-WS] Polling /latest via REST fallback [OHLCV-WS] Stream stale or chain down — isStale=true, chainDown=false, elapsed=44886ms [OHLCV-WS] Polling /latest via REST fallback ``` **Verify:** After the simulated disconnect, no more `Bar received` logs. REST polling kicks in every 15s once staleness threshold (30s) is exceeded. --- ## Group C — Requires Editing `.cjs` in node_modules > After testing, run `yarn install` in the mobile repo to restore the original file. --- ### Scenario 10: Subscribe Failure / Error Recovery **What this tests:** `OHLCVService.subscribe()` fails. The service catches the error, publishes `OHLCVService:subscriptionError`, forces reconnection, and REST fallback keeps the chart alive. #### Code to add **1. Disable dev simulation constant** in `useOHLCVRealtime.ts`: ```ts const DEV_SIMULATE_WS_DISCONNECT_AFTER_MS = 0; ``` **2. Simulate subscribe failure** — open `node_modules/@metamask/core-backend/dist/ws/ohlcv/OHLCVService.cjs`. Find the subscribe call (look for `BackendWebSocketService:subscribe`) and comment it out, then add a throw: ```js // await __classPrivateFieldGet(this, _OHLCVService_messenger, "f").call('BackendWebSocketService:subscribe', { // channels: [channel], // channelType: SUBSCRIPTION_NAMESPACE, // callback: (notification) => { // __classPrivateFieldGet(this, _OHLCVService_instances, "m", _OHLCVService_handleBarUpdate).call(this, channel, notification); // }, // }); throw new Error('DEV: Simulated subscribe failure — invalid channel'); ``` #### Steps to test 1. Apply both code changes above 3. Reload the app 4. Open Token Details for a supported token 5. Observe logs #### Expected — look for these key logs: ``` [OHLCV-WS] OHLCV-WS: Resubscribing active channels after reconnect {count: 0} [OHLCV-WS] Debounce fired — calling OHLCVService:subscribe for market-data.v1.eip155:8453/slip44:60.15m.eur [OHLCV-WS] OHLCV-WS: Subscription failed, forcing reconnection {channel: 'market-data.v1.eip155:8453/slip44:60.15m.eur', error: Error: Test error ...} [OHLCV-WS] Subscription error on market-data.v1.eip155:8453/slip44:60.15m.eur: Error: Test error (subscribe) [OHLCV-WS] OHLCV-WS: Forcing WebSocket reconnection [OHLCV-WS] OHLCV-WS: Resubscribing active channels after reconnect {count: 0} [OHLCV-WS] Stream stale or chain down — isStale=true, chainDown=false, elapsed=44219ms [OHLCV-WS] Polling /latest via REST fallback ``` **Verify:** Error is caught, reconnection attempted (`Forcing WebSocket reconnection`), and REST fallback keeps chart alive after staleness is detected. --- ## Log Reference All logs use the **`OHLCV-WS`** prefix. Filter by `OHLCV-WS` in Flipper / debugger. ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: Adds websocket streaming integration for ohlcv data ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-3194?atlOrigin=eyJpIjoiYmQ4N2E3MTlmZTFlNGYyNGFiODUxNzA2YThmM2FkYTkiLCJwIjoiaiJ9 Related: MetaMask/core#8695 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Introduces a new Engine-integrated WebSocket service and a real-time data path for price charts, which could impact app lifecycle, network subscriptions, and chart correctness if misconfigured. Includes a REST polling fallback and feature-flag gating, reducing blast radius but still touching core infrastructure. > > **Overview** > Adds real-time OHLCV candlestick streaming to the token details advanced chart by wiring `OHLCVService` (from `@metamask/core-backend`) into the Engine/messenger layer and upgrading `@metamask/core-backend` to `^6.3.0`. > > Introduces `useOHLCVRealtime`, which subscribes (debounced) to `OHLCVService` bar updates and provides a staleness/chain-down HTTP `/latest` fallback, then feeds updates into `AdvancedChart` via its existing `realtimeBar` prop. > > Gates the behavior behind a new remote, version-gated feature flag `tokenDetailsOhlcvWsIntegration` (registry + selector + CI constant mapping) and updates related unit tests/mocks to account for the new hook and selector. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit fe6f560. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Bernardo Garces Chapero <bernardo.chapero@consensys.net> Co-authored-by: Prithpal Sooriya <prithpal.sooriya@consensys.net>

Explanation
Architecture Overview
What
OHLCVServicefor real-time OHLCV (candlestick) data streaming via the backend WebSocket gatewayBackendWebSocketService,AccountActivityService) into a newsrc/ws/directory per code review feedbackWhy
ws/folder for better discoverabilityNew files
src/ws/ohlcv/OHLCVService.ts— main service with subscribe/unsubscribe semantics, reference counting, grace-period unsubscribe, idempotency checks, chain-status forwarding, and automatic resubscription on reconnectsrc/ws/ohlcv/OHLCVService.test.ts— 22 unit tests covering all paths (100% branch coverage)src/ws/ohlcv/OHLCVService-method-action-types.ts— auto-generated messenger action typessrc/ws/ohlcv/types.ts—OHLCVBarandOHLCVSubscriptionOptionstypessrc/ws/ohlcv/index.ts— barrel exportsModified files
src/index.ts— added exports forOHLCVService, its types, and allowed actions/events; updated import paths to./ws/eslint-suppressions.json— updated paths for moved files, added suppressions for new test fileCHANGELOG.md— documented new service and exportsMoved files (no logic changes)
src/BackendWebSocketService.ts→src/ws/BackendWebSocketService.tssrc/BackendWebSocketService.test.ts→src/ws/BackendWebSocketService.test.tssrc/BackendWebSocketService-method-action-types.ts→src/ws/BackendWebSocketService-method-action-types.tssrc/AccountActivityService.ts→src/ws/AccountActivityService.tssrc/AccountActivityService.test.ts→src/ws/AccountActivityService.test.tssrc/AccountActivityService-method-action-types.ts→src/ws/AccountActivityService-method-action-types.ts./logger→../logger,./types→../types, test helper paths)Key design decisions
AccountActivityService(auto-subscribes on account change),OHLCVServiceexposessubscribe()/unsubscribe()called by the UI when the chart mounts/unmountschannelHasSubscriptionbefore subscribing; duplicate calls are no-ops (React Strict Mode safe)system-notifications.v1.market-data.v1(auto-subscribed by server) and publishesOHLCVService:chainStatusChangedchainStatusChanged { status: 'down' }for all tracked chains, triggering UI polling fallbacksessionIdneeded for OHLCV; UI polling fallback covers the gap)init()method — system notification callback registered ininit()(not constructor) to comply with messenger-in-constructor lint ruleEvents published
OHLCVService:barUpdated—{ channel, bar: OHLCVBar }— new candle data from WebSocketOHLCVService:chainStatusChanged—{ chainIds, status, timestamp? }— chain up/down (server notification or WS disconnect)OHLCVService:subscriptionError—{ channel, error, operation }— subscribe or unsubscribe failureReferences
Checklist
Note
Medium Risk
Adds a new WebSocket-driven market-data service with reference counting, timers, and reconnect resubscription logic, which can affect subscription lifecycles and event delivery. Also moves existing WebSocket services into
src/ws/, so consumers relying on internal paths (vs package exports) could break if any remain.Overview
Adds a new
OHLCVServiceto stream real-time OHLCV bars over WebSocket, exposingsubscribe/unsubscribevia messenger actions, publishingbarUpdated/chainStatusChanged/subscriptionErrorevents, and handling reconnect resubscription with ref-counting plus a grace-period unsubscribe (mutex-protected).Refactors
core-backendby movingBackendWebSocketServiceandAccountActivityService(and their tests/action-type files) intosrc/ws/, updating imports/exports (src/index.ts), and updating lint suppressions; also addsasync-mutexplus comprehensive unit tests for the new service and documents the addition in the changelog.Reviewed by Cursor Bugbot for commit 730af62. Bugbot is set up for automated code reviews on this repo. Configure here.